package com.ssm.security.JWT;

import com.auth0.jwt.internal.org.apache.commons.lang3.StringUtils;
import com.ssm.security.JWTUserDetails;
import com.ssm.security.JWTUtils;
import com.ssm.security.UserStatus2Redis;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

/**
 * 主要为login服务的程序
 * 加载当前用户的信息
 * 生成令牌返回给前端
 * 解析当前令牌，获取其中的用户信息
 * 这里的userDetailsService不能为static，因为这个对象是通过security.xml配置注入的，加上static会报错，具体原因未知
 * 并且此处的userDetailsService只能用配置文件注入的方式才能正常的调用loadUserByUsername，不能直接新建对象进行调用，具体原因未知
 * 登录验证成功后要将用户存入redis，所以要注入JedisClientClusterImpl来对redis进行操作
 * 要使@Value能够取到properties中的值，TokenAuthenticationService必须以构造器注入的方式
 */
public class TokenAuthenticationService {
    static final String HEADER_REDIS_ID ="r_id";
    static final String HEADER_USER_NAME = "username";// 请求头用的参数username
    static final String HEADER_TOKEN_PREFIX = "Bearer "; // Token前缀,Bearer后面一个空格一定要保留
    static final String HEADER_AUTHORIZATION = "Authorization";// 存放Token的Header Key

    private JWTUtils jwtUtils;
    private static UserStatus2Redis userStatus2Redis;
    private UserDetailsService userDetailsService;

    static final Log logger = LogFactory.getLog(TokenAuthenticationService.class);

    @Value("${USER_LOGIN_NUM_LIMIT}")
    private Long USER_LOGIN_NUM_LIMIT;

    @Value("${USER_REDIS_TOKEN_EXPIRE_TIME}")
    private Integer USER_REDIS_TOKEN_EXPIRE_TIME;

    public JWTUtils getJwtUtils() {
        return jwtUtils;
    }

    public void setJwtUtils(JWTUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    public UserStatus2Redis getUserStatus2Redis() {
        return userStatus2Redis;
    }

    public void setUserStatus2Redis(UserStatus2Redis userStatus2Redis) {
        this.userStatus2Redis = userStatus2Redis;
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 根据用户名查出该用户的所有信息，为了生成token的时候将用户的部分信息放入token
     * @param username
     * @return
     */
    private JWTUserDetails jwtUserDetails(String username){
        JWTUserDetails jwtUserDetails = (JWTUserDetails) this.getUserDetailsService().loadUserByUsername(username);
        return jwtUserDetails;
    }

    /**
     * 根据用户信息生成token，同生成该用户当前设备登录在redis的辨识ID:r_id，用于存储与查找token的标签r_id，将用户名，r_id，token存入redis
     * 并且将token放入response返回给前端，前端如果访问受保护的资源就要在请求头放入token
     * 将当前用户请求的ip地址放入token，用来下次请求的安全校验
     * @param request
     * @param response
     * @param auth
     * @throws IOException
     */
    public void addAuthentication(HttpServletRequest request,HttpServletResponse response,Authentication auth) throws IOException {
        String userIPAddress = getUserIPAddress(request);
        JWTUserDetails jwtUserDetails = (JWTUserDetails) auth.getPrincipal();
        String JWT = jwtUtils.generateAccessToken(jwtUserDetails,userIPAddress);
        String r_id = UUID.randomUUID().toString();
        userStatus2Redis.saveUser2Redis(jwtUserDetails.getUsername(),r_id,JWT);
        logger.info("save" + "'"+jwtUserDetails.getUsername() +"'"+ " user token 2 redis");
        // 将 JWT 写入 response body
        try {
            if (!response.isCommitted()) {
                response.setContentType("application/json");
                response.setStatus(HttpServletResponse.SC_OK);
                response.setHeader("Authentication", JSONResult.fillResultString(200, "login_success", JWT, jwtUserDetails.getUsername(),r_id));
            }
        } catch (IOException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * 1.获取request请求头的token，并校验其规范
     * 2.如果token不为空则进行token_IP-request_IP校验
     * 3.如果取不出token中的IP，则视作token超时，超时就要将redis中的user-r_id删除和删除token，这样有个BUG，就是一但拿到用户的名字和r_id就可以对用户进行踢下线处理
     * 4.IP校验通过则进行request_token-redis_token 校验
     * 5.通过后进行token内部用户信息校验
     * 6.全部通过则进行生成authentication
     * 获取当前请求的token生成用户验证所需的UsernamePasswordAuthenticationToken
     * 进行IP过滤，用户本次访问IP需要与上次生成的token中的IP一样，否者不再进行token认证，视为匿名
     * 其中在生成前需要验证请求中的token的用户名是否与请求头中的username是否一样，为了多一种校验
     * ip不一样视作新设备访问，
     * 当token不为空，但是取不出ip时，视作token超时，超时就要将redis中的user-r_id删除和删除token
     *
     * 下一个版本可添加：
     * 1.针对IP不同的安全控制，提醒用户，或者冻结非法IP访问，不妥之处是若用户真的是掉线重拨，这时候是IP不一样了
     *
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    public static Authentication getAuthentication(HttpServletRequest request,HttpServletResponse response) throws IOException {
        JWTUtils jwtUtils = new JWTUtils();
        String auth_token = request.getHeader(HEADER_AUTHORIZATION);
        final String auth_token_start = HEADER_TOKEN_PREFIX;
        if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) {
            auth_token = auth_token.substring(auth_token_start.length());
        } else {
            logger.warn("user " + "'"+request.getHeader(HEADER_USER_NAME) +"'"+ " request a illegal token");
            auth_token = null;// 不按规范,不允许通过验证
        }

        if (auth_token !=null){
            String userIPAddress = getUserIPAddress(request);
            String username1 = request.getHeader(HEADER_USER_NAME);//取自http请求头的用户名，用于校验用户对应的token是不是相对应的
            String r_id=request.getHeader(HEADER_REDIS_ID);
            String tokenIPAddress = null;
            try {
                tokenIPAddress = jwtUtils.getUserIPAddress(auth_token);
            } catch (IOException e) {
                try {
                    userStatus2Redis.deleteUserStatus(username1,r_id);
                    logger.warn("user " + "'"+request.getHeader(HEADER_USER_NAME) +"'"+ " request a different IP address");
                } catch (IOException e1) {
                    throw new IOException(e.getMessage());
                }
                throw new IOException(e.getMessage());
            }
            if (userIPAddress.equals(tokenIPAddress)) {
                //取自token的用户名
                String username = null;
                try {
                    username = jwtUtils.getUsernameFromToken(auth_token);
                } catch (IOException e) {
                    throw new IOException(e.getMessage());
                }
                String r_JWT = userStatus2Redis.getUsertoken(r_id);
                boolean tokenAccess = auth_token.equals(r_JWT);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null/* && tokenAccess*/) {
                    JWTUserDetails jwtUserDetails = jwtUtils.getUserFromToken(auth_token);
                    if (jwtUtils.validateToken(auth_token, jwtUserDetails,username1)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jwtUserDetails, null, jwtUserDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        logger.info("user " + "'"+request.getHeader(HEADER_USER_NAME) +"'"+ " generate authentication success");
                        return authentication;
                    }

                }
            }
        }
        logger.info("user " + "'"+request.getHeader(HEADER_USER_NAME) +"'"+ " generate authentication failed");
        return null;
    }

    /**
     * 获取访问IP地址
     * @param request
     * @return
     */
    private static String getUserIPAddress(HttpServletRequest request) {
        return request.getRemoteAddr();
    }

}
